function LBXCustomBase64() {
  var _PADCHAR = "=",
    _ALPHA = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",
    _VERSION = "1.0";

  function _getbyte64(s, i) {
    // This is oddly fast, except on Chrome/V8.
    // Minimal or no improvement in performance by using a
    // object with properties mapping chars to value (eg. 'A': 0)

    var idx = _ALPHA.indexOf(s.charAt(i));

    if (idx === -1) {
      throw "Cannot decode base64";
    }

    return idx;
  }

  //+ (void)initCustomEncodeChars:(const char*)base64EncodeChars paddingChar:(char)paddingChar
  function _initCustomEncodeChars(base64EncodeChars, paddingChar) {
    if (base64EncodeChars) _ALPHA = base64EncodeChars;
    if (paddingChar) _PADCHAR = paddingChar;
  }

  function stringToBytes(str) {
    var ch,
            st,
            re = [];
    for (var i = 0; i < str.length; i++) {
      ch = str.charCodeAt(i); // get char
      st = []; // set up "stack"
      do {
        st.push(ch & 0xff); // push byte to stack
        ch = ch >> 8; // shift value down by 1 byte
      } while (ch);
      // add stack contents to result
      // done because chars have "wrong" endianness
      re = re.concat(st.reverse());
    }
    // return an array of bytes
    return re;
  }
  function _decode(s) {
    var pads = 0,
      i,
      b10,
      imax = s.length,
      x = [];

    // s = String(s);

    if (typeof(s)=='string')
    {
      s = String(s);
    }
    else {
      s = String.fromCharCode.apply(String, s);
    }

    if (imax === 0) {
      return s;
    }

    if (imax % 4 !== 0) {
      throw "Cannot decode base64";
    }

    if (s.charAt(imax - 1) === _PADCHAR) {
      pads = 1;

      if (s.charAt(imax - 2) === _PADCHAR) {
        pads = 2;
      }

      // either way, we want to ignore this last block
      imax -= 4;
    }

    for (i = 0; i < imax; i += 4) {
      b10 =
        (_getbyte64(s, i) << 18) |
        (_getbyte64(s, i + 1) << 12) |
        (_getbyte64(s, i + 2) << 6) |
        _getbyte64(s, i + 3);
      x.push(String.fromCharCode(b10 >> 16, (b10 >> 8) & 0xff, b10 & 0xff));
    }

    switch (pads) {
      case 1:
        b10 =
          (_getbyte64(s, i) << 18) |
          (_getbyte64(s, i + 1) << 12) |
          (_getbyte64(s, i + 2) << 6);
        x.push(String.fromCharCode(b10 >> 16, (b10 >> 8) & 0xff));
        break;

      case 2:
        b10 = (_getbyte64(s, i) << 18) | (_getbyte64(s, i + 1) << 12);
        x.push(String.fromCharCode(b10 >> 16));
        break;
    }

    let str = x.join("");

    return stringToBytes(str);
  }

  function _getbyte(s, i) {
    var x = s.charCodeAt(i);

    if (x > 255) {
      throw "INVALID_CHARACTER_ERR: DOM Exception 5";
    }

    return x;
  }

  function _encode(s) {
    if (arguments.length !== 1) {
      throw "SyntaxError: exactly one argument required";
    }

    if (typeof(s)=='string')
    {
      s = String(s);
    }
    else {
      s = String.fromCharCode.apply(String, s);
    }
    var i,
      b10,
      x = [],
      imax = s.length - (s.length % 3);

    if (s.length === 0) {
      return s;
    }

    for (i = 0; i < imax; i += 3) {
      b10 =
        (_getbyte(s, i) << 16) | (_getbyte(s, i + 1) << 8) | _getbyte(s, i + 2);
      x.push(_ALPHA.charAt(b10 >> 18));
      x.push(_ALPHA.charAt((b10 >> 12) & 0x3f));
      x.push(_ALPHA.charAt((b10 >> 6) & 0x3f));
      x.push(_ALPHA.charAt(b10 & 0x3f));
    }

    switch (s.length - imax) {
      case 1:
        b10 = _getbyte(s, i) << 16;
        x.push(
          _ALPHA.charAt(b10 >> 18) +
            _ALPHA.charAt((b10 >> 12) & 0x3f) +
            _PADCHAR +
            _PADCHAR
        );
        break;

      case 2:
        b10 = (_getbyte(s, i) << 16) | (_getbyte(s, i + 1) << 8);
        x.push(
          _ALPHA.charAt(b10 >> 18) +
            _ALPHA.charAt((b10 >> 12) & 0x3f) +
            _ALPHA.charAt((b10 >> 6) & 0x3f) +
            _PADCHAR
        );
        break;
    }

    return x.join("");
  }

  return {
      decode: _decode,
      encode: _encode,
      initCustomEncodeChars:_initCustomEncodeChars,
      VERSION: _VERSION
  };
}

export { LBXCustomBase64 };
